package javango.db;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.LogFactory;

public abstract class AbstractQuerySet<T> implements QuerySet<T> {

	protected Map<String, Object> valueFilters = new LinkedHashMap<String, Object>();
	protected Map<String, String> propertyFilters = new LinkedHashMap<String, String>();
	protected List<String> orderbys = new ArrayList<String>(); 
	
	protected Integer first;
	protected Integer last;
	
	// cached local copy of my count,  as I am immutable this can be cached once known.
	// subsequent calls to count() will always return the same value even if the underlying db changes.
	// i guess this could cause strangeness if the db was updated.  oh well DDN
	Long _count;
	
	protected Class<? extends T> clazz;
	
	// Create a new instance that is a clone of the other instance.
	protected AbstractQuerySet(AbstractQuerySet<T> other) {
		this.valueFilters.putAll(other.valueFilters);
		this.propertyFilters.putAll(other.propertyFilters);
		this.orderbys.addAll(other.orderbys);
		this.first = other.first;
		this.last = other.last;
		this.clazz = other.clazz;
	}
	
	protected AbstractQuerySet(Class<? extends T> clazz) throws ManagerException {
		this.clazz = clazz;
	}

	public abstract AbstractQuerySet<T> clone();

	public QuerySet<T> filter(Map<String, ?> params) throws ManagerException {
		return mapFilter(params);
	}
	
	public QuerySet<T> mapFilter(Map<String, ?> params) throws ManagerException {
		AbstractQuerySet<T> other = this.clone();
		// ignore null values,  this will change in .34
		// other.filters.putAll(params);
		for(Entry<String, ?> entry : params.entrySet()) {
			if (entry.getValue() != null) other.valueFilters.put(entry.getKey(), entry.getValue());
		}
		return other;
	}
	
	public QuerySet<T> filter(Object params) throws ManagerException {
		if (params instanceof Map) {
			return mapFilter((Map)params);
		}
		AbstractQuerySet<T> other = this.clone();

		try {
			Class c = params.getClass();
			java.lang.reflect.Field[] fields = c.getDeclaredFields();
			for (int i=0; i<fields.length; i++) {
				String fieldName = fields[i].getName();
				Object value = PropertyUtils.getProperty(params, fieldName);				
				if (value != null) other.valueFilters.put(fieldName, value);
			}
		} catch (IllegalAccessException e) {
			LogFactory.getLog(AbstractQuerySet.class).error(e,e);
			throw new ManagerException(e);
		} catch (InvocationTargetException e) {
			LogFactory.getLog(AbstractQuerySet.class).error(e,e);
			throw new ManagerException(e);
		} catch (NoSuchMethodException e) {
			LogFactory.getLog(AbstractQuerySet.class).error(e,e);
			throw new ManagerException(e);
		}	
		return other;
	}

	public QuerySet<T> filter(String property, Object value) throws ManagerException {
		AbstractQuerySet<T> other = this.clone();
		other.valueFilters.put(property, value);
		return other;
	}

	public QuerySet<T> filterByProperty(String propertyA, String propertyB) throws ManagerException {
		AbstractQuerySet<T> other = this.clone();
		other.propertyFilters.put(propertyA, propertyB);
		return other;
	}
	
	public QuerySet<T> limit(int first, int last) throws ManagerException {
		AbstractQuerySet<T> other = this.clone();
		other.first = first;
		other.last = last;
		return other;
	}
	
	public abstract T get() throws ManagerException;

	public abstract List<T> list() throws ManagerException;
	
	public Iterator<T> iterator() {
		try {
			return list().iterator();
		} catch (ManagerException e) {
			// TODO What to do with this exception
			throw new RuntimeException(e);
		}
	}

	public QuerySet<T> orderBy(String... orderbys) throws ManagerException {
		AbstractQuerySet<T> other = this.clone();
		for(String order : orderbys) {
			other.orderbys.add(order);
		}
		return other;
	}
	
	// impelemtation specific functions	
	public abstract long count() throws ManagerException;

	public Class<? extends T> getType() {
		return clazz;
	}
}
